Skip to content

feat: git-backed pipeline audit trail#19

Merged
TerrifiedBug merged 15 commits intomainfrom
feat/git-backed-pipelines
Mar 6, 2026
Merged

feat: git-backed pipeline audit trail#19
TerrifiedBug merged 15 commits intomainfrom
feat/git-backed-pipelines

Conversation

@TerrifiedBug
Copy link
Owner

Summary

  • Add Git integration to environments — configure a repo URL, branch, and encrypted PAT to automatically commit pipeline YAML on deploy/delete
  • New git-sync service handles clone/commit/push in temp directories with non-blocking error handling (failures never gate deploys)
  • Git Integration settings UI on environment detail page with test connection, save, and disconnect actions
  • Deploy dialog shows warning toast when git sync fails
  • Tokens encrypted with AES-256-GCM (existing pattern), HTTPS-only URLs enforced, author strings sanitized

Changes

Area Files
Schema prisma/schema.prisma — 3 nullable fields on Environment
Service src/server/services/git-sync.ts — commit/delete pipeline YAML
Deploy hook src/server/services/deploy-agent.ts — post-deploy git sync
Delete hook src/server/routers/pipeline.ts — pre-delete git sync
Router src/server/routers/environment.ts — git config CRUD + test connection
UI src/components/environment/git-sync-section.tsx — settings card
UX src/components/flow/deploy-dialog.tsx — git sync warning toast
Docs docs/public/user-guide/environments.md, docs/public/operations/security.md

Test plan

  • Configure git integration on an environment (repo URL, branch, PAT)
  • Test Connection button verifies repo access
  • Deploy a pipeline — verify YAML committed to {env-name}/{pipeline-name}.yaml
  • Deploy same pipeline again — verify commit updates the file
  • Delete a pipeline — verify YAML file removed from repo
  • Disconnect git integration — verify fields cleared
  • Deploy with invalid PAT — verify deploy succeeds with warning toast
  • Verify encrypted token is not leaked in API responses

Implements the core git-sync service that handles cloning, committing,
and pushing pipeline YAML files to a configured Git repository. Supports
both commit (deploy) and delete operations with temp directory cleanup
and non-throwing error handling.
Call gitSyncCommitPipeline after version creation in deployAgent.
The sync runs as a non-blocking side effect — if it fails, the deploy
still succeeds and the error is surfaced via gitSyncError in the result.
- Strip encrypted gitToken from environment.get API response (defense-in-depth)
- Clone into subdirectory of temp dir to avoid non-empty directory errors
- Add HTTPS-only validation for git repo URLs (SSRF protection)
- Add branch name regex validation
- Sanitize git author name/email to prevent malformed author strings
- Handle file-not-found gracefully in gitSyncDeletePipeline
@github-actions github-actions bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file feature labels Mar 6, 2026
@greptile-apps
Copy link

greptile-apps bot commented Mar 6, 2026

Greptile Summary

This PR adds a git-backed audit trail for pipeline deployments — on each deploy or delete, VectorFlow commits the generated pipeline YAML to a configured HTTPS Git repository, with the PAT encrypted at rest using the existing AES-256-GCM pattern. Several issues from earlier review rounds have been properly addressed: withAudit and environmentId are present on testGitConnection, error messages are sanitized before logging or returning, committer identity is configured before each commit, toFilenameSlug now returns "unnamed" as a safe fallback, and UI state management (toast messaging, loading state) is correct.

Two issues remain that should be addressed before merging:

  • SSRF in testGitConnection and saved gitRepoUrl: The server performs outbound git clone to any user-supplied HTTPS URL without validating whether the hostname resolves to a private/loopback address. An authenticated EDITOR can probe internal HTTPS services (Kubernetes API, internal tooling) via error timing and messages. A private-range blocklist should be applied before initiating any git network operation.
  • Shallow-clone push fails under concurrent deploys: Both commit and delete functions use --depth 1, so concurrent deploys to the same environment will produce non-fast-forward push rejections on all but the first, resulting in repeated git sync warning toasts. A retry loop or removal of --depth 1 would improve reliability.
  • Record<string, unknown> in update mutation: The Prisma update data object loses type-checking; using Prisma.EnvironmentUpdateInput directly would restore compile-time safety.

Confidence Score: 3/5

  • Mergeable with caution — the SSRF issue should be fixed before deploying to network environments where internal HTTPS services are reachable by the VectorFlow server process.
  • Core functionality is solid: token encryption, error sanitization, committer identity, UI state management, and audit logging are all correctly implemented. Two reliability/security concerns (SSRF via unrestricted outbound git connections, and shallow-clone race condition on concurrent deploys) should be resolved before merging into production. The Record<string, unknown> type issue is a code-quality concern but not blocking.
  • src/server/routers/environment.ts (SSRF in testGitConnection, type safety in update), src/server/services/git-sync.ts (concurrent deploy push failure)

Important Files Changed

Filename Overview
src/server/services/git-sync.ts New service for clone/commit/push; token sanitization and committer identity are correctly handled. Concurrent deploys will cause non-fast-forward push failures due to --depth 1 with no retry logic.
src/server/routers/environment.ts Adds testGitConnection mutation with withAudit, withTeamAccess, and error sanitization; introduces an SSRF risk since user-supplied HTTPS URLs are cloned without blocking private/loopback addresses. update mutation uses Record<string, unknown> which loses Prisma type-checking.
src/server/routers/pipeline.ts Git sync fire-and-forget added to delete mutation, but the await on the git operation still runs before prisma.pipeline.delete, which means the DB delete won't happen until git completes (even though errors are swallowed).
src/server/services/deploy-agent.ts Git sync result is propagated back to the caller correctly; gitSyncError is added to AgentDeployResult. The await is synchronous relative to the deploy response (latency concern already tracked).
src/components/environment/git-sync-section.tsx New settings card for git integration; disconnect state resets are correctly in onSuccess, per-call toasts are context-specific, and setIsTesting is only called when actually initiating a test.
prisma/schema.prisma Three nullable fields added to Environment; migration SQL is consistent with schema changes.
src/components/flow/deploy-dialog.tsx Git sync warning toast is correctly shown after successful deploy if gitSyncError is present; duration set to 8 seconds for visibility.

Sequence Diagram

sequenceDiagram
    participant U as User (Browser)
    participant R as environment.ts router
    participant DA as deploy-agent.ts
    participant GS as git-sync.ts
    participant DB as PostgreSQL
    participant GR as Git Remote

    U->>R: testGitConnection(repoUrl, branch, token)
    R->>R: HTTPS check, withTeamAccess("EDITOR")
    R->>GR: git clone --depth 1 (token in URL)
    GR-->>R: success / error (sanitized)
    R-->>U: { success, error? }

    U->>DA: deploy.agent(pipelineId)
    DA->>DA: createVersion, push to nodes
    DA->>DB: findUnique(environment)
    DB-->>DA: { gitRepoUrl, gitToken (encrypted) }
    DA->>GS: gitSyncCommitPipeline(config, yaml)
    GS->>GS: decrypt(encryptedToken)
    GS->>GR: git clone --depth 1
    GS->>GR: git add, commit, push
    GR-->>GS: success / non-fast-forward error
    GS-->>DA: { success, error? }
    DA-->>U: { versionId, gitSyncError? }

    U->>R: pipeline.delete(id)
    R->>GS: gitSyncDeletePipeline (awaited)
    GS->>GR: git clone --depth 1, rm, commit, push
    GR-->>GS: result
    GS-->>R: (error swallowed via .catch)
    R->>DB: prisma.pipeline.delete
    DB-->>R: done
    R-->>U: deleted pipeline
Loading

Last reviewed commit: e38c3f6

- Sanitize git error messages to strip credentials from URLs
- Add withAudit middleware to testGitConnection mutation
- Add environmentId to testGitConnection for proper team scoping
Comment on lines +152 to +175
.mutation(async ({ input }) => {
const parsedUrl = new URL(input.repoUrl);
if (parsedUrl.protocol !== "https:") {
return { success: false, error: "Only HTTPS repository URLs are supported" };
}

const simpleGit = (await import("simple-git")).default;
const { mkdtemp, rm } = await import("fs/promises");
const { join } = await import("path");
const { tmpdir } = await import("os");

let workdir: string | null = null;
try {
workdir = await mkdtemp(join(tmpdir(), "vf-git-test-"));
const repoDir = join(workdir, "repo");
const git = simpleGit(workdir);
parsedUrl.username = input.token;
parsedUrl.password = "";
await git.clone(parsedUrl.toString(), repoDir, [
"--branch", input.branch,
"--depth", "1",
"--single-branch",
]);
return { success: true };
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SSRF via user-supplied repo URL

testGitConnection initiates an outbound git clone to any HTTPS URL supplied by an authenticated EDITOR. The HTTPS-only check guards against the most common HTTP-based cloud metadata endpoints (e.g., AWS at http://169.254.169.254/latest/meta-data/), but internal services that are HTTPS-accessible — such as a private Kubernetes API server, internal Nexus/Artifactory, or a corporate identity provider — can still be reached.

An EDITOR in one team can use this endpoint to probe whether internal services are reachable by observing error responses and timing differences, without ever having access to those services themselves.

Consider adding a private IP / loopback blocklist before the clone attempt:

const BLOCKED_HOSTS = /^(localhost|127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|::1|fc00:|fd)/i;
const hostname = parsedUrl.hostname;
if (BLOCKED_HOSTS.test(hostname)) {
  return { success: false, error: "Private and loopback addresses are not allowed" };
}

The same guard should be applied inside gitSyncCommitPipeline and gitSyncDeletePipeline in git-sync.ts, since a saved gitRepoUrl set via the update mutation is also passed to those functions with the stored (decrypted) PAT.

Context Used: Rule from dashboard - ## Security & Cryptography Review Rules

When reviewing changes to authentication, authorization, en... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/server/routers/environment.ts
Line: 152-175

Comment:
**SSRF via user-supplied repo URL**

`testGitConnection` initiates an outbound `git clone` to any HTTPS URL supplied by an authenticated EDITOR. The HTTPS-only check guards against the most common HTTP-based cloud metadata endpoints (e.g., AWS at `http://169.254.169.254/latest/meta-data/`), but internal services that are HTTPS-accessible — such as a private Kubernetes API server, internal Nexus/Artifactory, or a corporate identity provider — can still be reached.

An EDITOR in one team can use this endpoint to probe whether internal services are reachable by observing error responses and timing differences, without ever having access to those services themselves.

Consider adding a private IP / loopback blocklist before the clone attempt:

```typescript
const BLOCKED_HOSTS = /^(localhost|127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|::1|fc00:|fd)/i;
const hostname = parsedUrl.hostname;
if (BLOCKED_HOSTS.test(hostname)) {
  return { success: false, error: "Private and loopback addresses are not allowed" };
}
```

The same guard should be applied inside `gitSyncCommitPipeline` and `gitSyncDeletePipeline` in `git-sync.ts`, since a saved `gitRepoUrl` set via the `update` mutation is also passed to those functions with the stored (decrypted) PAT.

**Context Used:** Rule from `dashboard` - ## Security & Cryptography Review Rules

When reviewing changes to authentication, authorization, en... ([source](https://app.greptile.com/review/custom-context?memory=7cb20c56-ca6a-40aa-8660-7fa75e6e3db2))

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +77 to +103
const git: SimpleGit = simpleGit(workdir);
await git.clone(url, repoDir, ["--branch", config.branch, "--depth", "1", "--single-branch"]);
const repoGit: SimpleGit = simpleGit(repoDir);

// Write the pipeline YAML file
const envDir = toFilenameSlug(environmentName);
const filename = `${toFilenameSlug(pipelineName)}.yaml`;
const filePath = join(envDir, filename);
const fullPath = join(repoDir, filePath);

await mkdir(join(repoDir, envDir), { recursive: true });
await writeFile(fullPath, configYaml, "utf-8");

await repoGit.add(filePath);

// Check if there are actually changes to commit
const status = await repoGit.status();
if (status.isClean()) {
return { success: true, commitSha: "no-change" };
}

await repoGit.addConfig("user.name", author.name || "VectorFlow User");
await repoGit.addConfig("user.email", author.email || "noreply@vectorflow");
await repoGit.commit(commitMessage, filePath, {
"--author": sanitizeAuthor(author.name, author.email),
});
await repoGit.push("origin", config.branch);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shallow-clone push fails on concurrent deploys

Both gitSyncCommitPipeline and gitSyncDeletePipeline use --depth 1 clones. If two pipelines in the same environment are deployed concurrently (or a deploy and a delete overlap), the sequence becomes:

  1. Deploy A clones branch at commit X
  2. Deploy B clones branch at commit X
  3. Deploy A pushes commit Y — succeeds
  4. Deploy B tries to push commit Y'rejected: non-fast-forward (remote is now at Y)

Because the local repo has only one commit of history (--depth 1), there is no way to run git pull --rebase to recover. The push fails, and the user sees a git sync warning toast even though the deploy itself succeeded. In environments with pipelines frequently deployed together (e.g., automated CI deploys), this failure mode will be common.

Options:

  • Remove --depth 1 to allow git pull --rebase before pushing (adds latency)
  • Retry the entire clone→commit→push cycle on non-fast-forward failures (up to N times)
  • Document this limitation explicitly in the user guide

The same issue applies to gitSyncDeletePipeline (line 137).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/server/services/git-sync.ts
Line: 77-103

Comment:
**Shallow-clone push fails on concurrent deploys**

Both `gitSyncCommitPipeline` and `gitSyncDeletePipeline` use `--depth 1` clones. If two pipelines in the same environment are deployed concurrently (or a deploy and a delete overlap), the sequence becomes:

1. Deploy A clones branch at commit **X**
2. Deploy B clones branch at commit **X**
3. Deploy A pushes commit **Y** — succeeds
4. Deploy B tries to push commit **Y'****rejected: non-fast-forward** (remote is now at Y)

Because the local repo has only one commit of history (`--depth 1`), there is no way to run `git pull --rebase` to recover. The push fails, and the user sees a git sync warning toast even though the deploy itself succeeded. In environments with pipelines frequently deployed together (e.g., automated CI deploys), this failure mode will be common.

Options:
- Remove `--depth 1` to allow `git pull --rebase` before pushing (adds latency)
- Retry the entire clone→commit→push cycle on non-fast-forward failures (up to N times)
- Document this limitation explicitly in the user guide

The same issue applies to `gitSyncDeletePipeline` (line 137).

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +126 to 133
const data: Record<string, unknown> = { ...rest };
if (gitToken !== undefined) {
data.gitToken = gitToken ? encrypt(gitToken) : null;
}

const updated = await prisma.environment.update({
where: { id },
data,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Record<string, unknown> bypasses Prisma type safety

Using Record<string, unknown> as the type for data loses all compile-time checking for the Prisma update. Field name typos and type mismatches won't be caught by tsc, and unknown fields would be silently ignored by Prisma at runtime.

Consider using the Prisma-generated input type (Prisma.EnvironmentUpdateInput) and constructing the object with explicit field assignments. This would restore TypeScript's ability to catch invalid field names and incorrect value types before runtime.

Context Used: Rule from dashboard - ## Code Style & Conventions

TypeScript Conventions

  • Strict mode enabled, avoid any — use `un... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/server/routers/environment.ts
Line: 126-133

Comment:
**`Record<string, unknown>` bypasses Prisma type safety**

Using `Record<string, unknown>` as the type for `data` loses all compile-time checking for the Prisma update. Field name typos and type mismatches won't be caught by `tsc`, and unknown fields would be silently ignored by Prisma at runtime.

Consider using the Prisma-generated input type (`Prisma.EnvironmentUpdateInput`) and constructing the object with explicit field assignments. This would restore TypeScript's ability to catch invalid field names and incorrect value types before runtime.

**Context Used:** Rule from `dashboard` - ## Code Style & Conventions

### TypeScript Conventions
- Strict mode enabled, avoid `any` — use `un... ([source](https://app.greptile.com/review/custom-context?memory=6ae51394-d0b6-4686-bc4c-1ad840c2e310))

How can I resolve this? If you propose a fix, please make it concise.

@TerrifiedBug TerrifiedBug merged commit 41089a3 into main Mar 6, 2026
10 checks passed
@TerrifiedBug TerrifiedBug deleted the feat/git-backed-pipelines branch March 6, 2026 13:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant